# HOT-обновления в PostgreSQL и управление нагрузкой

## Область применения

Документ ориентирован на **технические таблицы ERP-систем**:

- Таблицы свёрток и агрегатов.
- Регистры и промежуточные расчётные данные.
- Таблицы состояний, балансов, накоплений.
- Staging- и buffer-таблицы.

Для таких таблиц характерны:

- Большое количество операций `UPDATE`.
- Повторное обновление одних и тех же строк.
- Высокая чувствительность к bloat, WAL и частоте `VACUUM`.

Для пользовательских таблиц (справочники, транзакции) HOT обычно не критичен и редко требует настройки.

## Как работает UPDATE в PostgreSQL

### Ключевой факт

```{note}
Любой обычный `UPDATE` в PostgreSQL обновляет все индексы таблицы, даже если изменяемые столбцы в индексах не участвуют.
```
Это следует из MVCC-модели: при обновлении создаётся новая версия строки с новым TID (`ctid`). Поскольку индексы хранят ссылки на TID, они обязаны быть обновлены.

### Почему так происходит

- `UPDATE` не изменяет строку на месте.
- Создаётся новая версия строки.
- Новая версия получает новый TID.
- Все индексы ссылаются на TID, а не на данные.

Следовательно, даже обновление неиндексированного числового поля приводит к обновлению всех индексов.

### Иллюстрация обычного UPDATE

```
Heap
+-+
| (5,12) version 1 |
+-+
| (8,42) version 2 |  <-- новая версия строки
+-+

Indexes (любые, даже 100 штук)
idx_a -> (5,12)  [устарело]
idx_b -> (5,12)  [устарело]
idx_c -> (5,12)  [устарело]

После UPDATE:

idx_a -> (8,42)  [актуально]
idx_b -> (8,42)  [актуально]
idx_c -> (8,42)  [актуально]
```

## HOT: единственное исключение

**HOT (Heap-Only Tuple)** — оптимизация, позволяющая выполнять `UPDATE` без обновления индексов.

### Суть HOT

HOT возможен, если PostgreSQL может:

- Сохранить старый TID в индексах.
- Связать новую версию строки с предыдущей внутри одной heap-страницы.

### UPDATE в HOT-режиме

```
Heap page
+--+
| (17,3) version 1         |  <-- индекс указывает сюда
|        |                 |
|        v HOT chain       |
| (17,4) version 2 (HOT)   |
+--+

Indexes
idx_a -> (17,3)
idx_b -> (17,3)
idx_c -> (17,3)
```

Индекс продолжает указывать на старый TID. При чтении PostgreSQL проходит HOT-цепочку и находит актуальную версию строки.

```{note}
Это единственная причина, по которой HOT не требует обновления индексов.
```

## Когда HOT невозможен

HOT невозможен, если индекс больше не может безопасно указывать на старый TID.

### Expression-индексы

```sql
CREATE INDEX ON t ((payload->>'status'));
```

Если `UPDATE` изменяет результат выражения, индекс обязан указывать на новую версию строки -> HOT невозможен.

### Partial-индексы

```sql
CREATE INDEX ON t(id) WHERE active;
```

Если `UPDATE` меняет значение условия `WHERE`, строка может быть добавлена в индекс или удалена из него -> HOT невозможен.

### Недостаток места на heap-странице

Даже при логической возможности HOT он не сработает, если:

- `fillfactor = 100`;
- страницы плотно заполнены;
- новая версия строки не помещается на той же странице.

В этом случае создаётся новая heap-строка -> HOT невозможен.

### Триггеры, изменяющие индексируемые поля

Типовой сценарий:

- `UPDATE` меняет техническое поле.
- `BEFORE UPDATE`-триггер обновляет `updated_at`.
- Столбец `updated_at` индексирован.

HOT невозможен, несмотря на «безопасный» SQL.

## Диагностика: поиск таблиц без HOT

### Основной запрос

Используйте статистику PostgreSQL для выявления проблемных таблиц:

```sql
SELECT
    relname                         AS table_name,
    n_tup_upd                       AS updates,
    n_tup_hot_upd                   AS hot_updates,
    round(
        100.0 * n_tup_hot_upd / NULLIF(n_tup_upd, 0),
        2
    )                               AS hot_ratio_percent
FROM pg_stat_user_tables
WHERE n_tup_upd > 100000
ORDER BY hot_ratio_percent ASC;
```

### Интерпретация результатов

- **`hot_ratio < 20%`** — таблица активно обновляется без HOT.
- **`hot_ratio 70–95%`** — оптимально для технических таблиц.
- **`hot_ratio = 0`** — HOT принципиально невозможен или таблица спроектирована неверно.

### Косвенные признаки

Большое количество «мёртвых» строк при активных `UPDATE` также указывает на отсутствие HOT:

```sql
SELECT
    relname,
    n_tup_upd,
    n_dead_tup
FROM pg_stat_user_tables
WHERE n_tup_upd > 0
ORDER BY n_dead_tup DESC;
```

## Управление HOT: fillfactor таблицы

### Что делает fillfactor

Параметр `fillfactor` задаёт, сколько места PostgreSQL оставляет свободным на heap-страницах для будущих `UPDATE`.

Пример:
```sql
ALTER TABLE tech_table SET (fillfactor = 70);
```

Это означает, что ~30% страницы остаётся свободной, что повышает вероятность размещения новых версий строк на той же странице.

### Рекомендуемые значения

| Тип таблицы      | fillfactor |
|||
| Пользовательские | 100        |
| Технические      | 60–80      |
| Очень «горячие»  | 50–60      |

```{attention} 
`fillfactor` — это компромисс между производительностью `UPDATE` и размером таблицы. Применяйте его только для таблиц с высокой нагрузкой.
```

### Важное ограничение

Команда:
```sql
ALTER TABLE ... SET (fillfactor = X);
```
- Не перепаковывает существующие данные.
- Эффект проявляется постепенно, по мере выполнения `UPDATE`.
- Для немедленного эффекта используйте `VACUUM FULL`, `CLUSTER` или `pg_repack`.

## Fillfactor индексов

### Что это и зачем

Индексы (особенно B-tree) имеют собственный параметр `fillfactor`. По умолчанию он равен **90**.

```{attention}
`fillfactor` индекса **не влияет на возможность HOT**. HOT определяется только heap-страницей.
```

Однако при отсутствии HOT каждый `UPDATE` обновляет все индексы. Низкий `fillfactor` индекса снижает частоту page split и рост bloat.

### Рекомендации 

Для технических таблиц с активными `UPDATE`:

- Устанавливайте `fillfactor` индексов в диапазоне **70–80**.
- Особенно актуально для крупных B-tree-индексов.

Пример:
```sql
ALTER INDEX idx_big SET (fillfactor = 75);
REINDEX INDEX idx_big;
```

Это не ускоряет HOT, но снижает ущерб, когда HOT невозможен.

### Итоги

- Любой обычный `UPDATE` обновляет все индексы таблицы.
- HOT — единственный механизм, позволяющий избежать этого.
- HOT критичен для технических таблиц с массовыми `UPDATE`.
- Основной рычаг управления HOT — `fillfactor` таблицы.
- Снижение `fillfactor` — осознанный компромисс между размером и производительностью.
- `fillfactor` индексов не влияет на HOT, но важен для контроля bloat при отсутствии HOT.